// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <assert.h>
#include <endian.h>
#include <fuchsia/hardware/bluetooth/c/fidl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
#include <threads.h>
#include <zircon/assert.h>
#include <zircon/status.h>
#include <zircon/threads.h>

#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <ddk/metadata.h>
#include <ddk/platform-defs.h>
#include <ddk/protocol/bt/hci.h>
#include <ddk/protocol/serial.h>

// TODO: how can we parameterize this?
#define TARGET_BAUD_RATE 2000000

#define MAC_ADDR_LEN 6

// TODO: Determine firmware name based on controller version.
#define FIRMWARE_PATH "BCM4345C5.hcd"

#define FIRMWARE_DOWNLOAD_DELAY ZX_MSEC(50)

// Hardcoded. Better to parameterize on chipset.
// Broadcom chips need a few hundred msec delay
// after firmware load
#define BAUD_RATE_SWITCH_DELAY ZX_MSEC(200)

typedef struct {
  uint16_t opcode;
  uint8_t parameter_total_size;
} __PACKED hci_command_header_t;

typedef struct {
  uint8_t event_code;
  uint8_t parameter_total_size;
} __PACKED hci_event_header_t;

typedef struct {
  hci_event_header_t header;
  uint8_t num_hci_command_packets;
  uint16_t command_opcode;
  uint8_t return_code;
} __PACKED hci_command_complete_t;

typedef struct {
  hci_event_header_t header;
  uint8_t num_hci_command_packets;
  uint16_t command_opcode;
  uint8_t return_code;
  uint8_t bdaddr[MAC_ADDR_LEN];
} __PACKED hci_read_bdaddr_command_complete_t;

// HCI reset command
const hci_command_header_t RESET_CMD = {
    .opcode = 0x0c03,
    .parameter_total_size = 0,
};

// vendor command to begin firmware download
const hci_command_header_t START_FIRMWARE_DOWNLOAD_CMD = {
    .opcode = 0xfc2e,
    .parameter_total_size = 0,
};

// HCI command to read BDADDR from controller
const hci_command_header_t READ_BDADDR_CMD = {
    .opcode = 0x1009,
    .parameter_total_size = 0,
};

typedef struct {
  hci_command_header_t header;
  uint16_t unused;
  uint32_t baud_rate;
} __PACKED bcm_set_baud_rate_cmd_t;
#define BCM_SET_BAUD_RATE_CMD 0xfc18

typedef struct {
  hci_command_header_t header;
  uint8_t bdaddr[MAC_ADDR_LEN];
} __PACKED bcm_set_bdaddr_cmd_t;
#define BCM_SET_BDADDR_CMD 0xfc01

#define HCI_EVT_COMMAND_COMPLETE 0x0e

typedef struct {
  zx_device_t* zxdev;
  zx_device_t* transport_dev;
  bt_hci_protocol_t hci;
  serial_protocol_t serial;
  zx_handle_t command_channel;
  bool is_uart;  // true if underlying transport is UART
} bcm_hci_t;

static zx_status_t bcm_hci_get_protocol(void* ctx, uint32_t proto_id, void* out_proto) {
  if (proto_id != ZX_PROTOCOL_BT_HCI) {
    return ZX_ERR_NOT_SUPPORTED;
  }

  bcm_hci_t* hci = ctx;
  bt_hci_protocol_t* hci_proto = out_proto;

  // Forward the underlying bt-transport ops.
  hci_proto->ops = hci->hci.ops;
  hci_proto->ctx = hci->hci.ctx;

  return ZX_OK;
}

static void bcm_hci_unbind(void* ctx) {
  bcm_hci_t* hci = ctx;

  device_remove(hci->zxdev);
}

static void bcm_hci_release(void* ctx) {
  bcm_hci_t* hci = ctx;

  if (hci->command_channel != ZX_HANDLE_INVALID) {
    zx_handle_close(hci->command_channel);
  }

  free(hci);
}

zx_status_t fidl_bt_hci_open_command_channel(void* ctx, zx_handle_t channel) {
  bcm_hci_t* hci = ctx;
  zx_status_t status = bt_hci_open_command_channel(&hci->hci, channel);
  if (status != ZX_OK) {
    zx_handle_close(channel);
  }
  return status;
}

zx_status_t fidl_bt_hci_open_acl_data_channel(void* ctx, zx_handle_t channel) {
  bcm_hci_t* hci = ctx;
  zx_status_t status = bt_hci_open_acl_data_channel(&hci->hci, channel);
  if (status != ZX_OK) {
    zx_handle_close(channel);
  }
  return status;
}

zx_status_t fidl_bt_hci_open_snoop_channel(void* ctx, zx_handle_t channel) {
  bcm_hci_t* hci = ctx;
  zx_status_t status = bt_hci_open_snoop_channel(&hci->hci, channel);
  if (status != ZX_OK) {
    zx_handle_close(channel);
  }
  return status;
}

static fuchsia_hardware_bluetooth_Hci_ops_t fidl_ops = {
    .OpenCommandChannel = fidl_bt_hci_open_command_channel,
    .OpenAclDataChannel = fidl_bt_hci_open_acl_data_channel,
    .OpenSnoopChannel = fidl_bt_hci_open_snoop_channel,
};

static zx_status_t fuchsia_bt_hci_message_instance(void* ctx, fidl_msg_t* msg, fidl_txn_t* txn) {
  return fuchsia_hardware_bluetooth_Hci_dispatch(ctx, txn, msg, &fidl_ops);
}

static zx_protocol_device_t bcm_hci_device_proto = {
    .version = DEVICE_OPS_VERSION,
    .get_protocol = bcm_hci_get_protocol,
    .message = fuchsia_bt_hci_message_instance,
    .unbind = bcm_hci_unbind,
    .release = bcm_hci_release,
};

static zx_status_t bcm_hci_send_command(bcm_hci_t* hci, const hci_command_header_t* command,
                                        size_t length, void* out_buf, size_t out_buf_len) {
#define CHAN_READ_BUF_LEN 257
  uint8_t read_buf[CHAN_READ_BUF_LEN];
  if (out_buf_len > CHAN_READ_BUF_LEN) {
    zxlogf(ERROR, "bcm_hci_send_command provided |out_buf| is too large");
    return ZX_ERR_INVALID_ARGS;
  }

  // send HCI command
  zx_status_t status = zx_channel_write(hci->command_channel, 0, command, length, NULL, 0);
  if (status != ZX_OK) {
    zxlogf(ERROR, "bcm_hci_send_command zx_channel_write failed %s\n",
           zx_status_get_string(status));
    return status;
  }

  // wait for an HCI Command Complete event
  uint32_t actual;

  do {
    status = zx_channel_read(hci->command_channel, 0, read_buf, NULL, CHAN_READ_BUF_LEN, 0, &actual,
                             NULL);
    if (status == ZX_ERR_SHOULD_WAIT) {
      zx_object_wait_one(hci->command_channel, ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED,
                         zx_deadline_after(ZX_SEC(5)), NULL);
    }
  } while (status == ZX_ERR_SHOULD_WAIT);

  if (status != ZX_OK) {
    zxlogf(ERROR, "bcm_hci_send_command zx_channel_read failed %s\n", zx_status_get_string(status));
    return status;
  }

  hci_event_header_t* header = (hci_event_header_t*)read_buf;
  if (header->event_code != HCI_EVT_COMMAND_COMPLETE ||
      header->parameter_total_size != sizeof(hci_command_complete_t) - sizeof(hci_event_header_t)) {
    zxlogf(ERROR, "bcm_hci_send_command did not receive command complete\n");
    return ZX_ERR_INTERNAL;
  }
  hci_command_complete_t* event = (hci_command_complete_t*)read_buf;
  if (event->return_code != 0) {
    zxlogf(ERROR, "bcm_hci_send_command got command complete error %u\n", event->return_code);
    return ZX_ERR_INTERNAL;
  }

  if (out_buf) {
    memcpy(out_buf, read_buf, out_buf_len);
  }

  return ZX_OK;
}

static zx_status_t bcm_hci_set_baud_rate(bcm_hci_t* hci, uint32_t baud_rate) {
  bcm_set_baud_rate_cmd_t command = {
      .header =
          {
              .opcode = BCM_SET_BAUD_RATE_CMD,
              .parameter_total_size =
                  sizeof(bcm_set_baud_rate_cmd_t) - sizeof(hci_command_header_t),
          },
      .unused = 0,
      .baud_rate = htole32(baud_rate),
  };

  zx_status_t status = bcm_hci_send_command(hci, &command.header, sizeof(command), NULL, 0);
  if (status != ZX_OK) {
    return status;
  }

  return serial_config(&hci->serial, TARGET_BAUD_RATE, SERIAL_SET_BAUD_RATE_ONLY);
}

static zx_status_t bcm_hci_set_bdaddr(bcm_hci_t* hci, uint8_t bdaddr[MAC_ADDR_LEN]) {
  bcm_set_bdaddr_cmd_t command = {
      .header =
          {
              .opcode = BCM_SET_BDADDR_CMD,
              .parameter_total_size = sizeof(bcm_set_bdaddr_cmd_t) - sizeof(hci_command_header_t),
          },
      .bdaddr =
          {// HCI expects little endian. Swap bytes
           bdaddr[5], bdaddr[4], bdaddr[3], bdaddr[2], bdaddr[1], bdaddr[0]},
  };

  return bcm_hci_send_command(hci, &command.header, sizeof(command), NULL, 0);
}

static zx_status_t bcm_get_bdaddr_from_bootloader(bcm_hci_t* hci, uint8_t macaddr[MAC_ADDR_LEN]) {
  uint8_t bootloader_macaddr[8];
  size_t actual_len;
  zx_status_t status =
      device_get_metadata(hci->zxdev, DEVICE_METADATA_MAC_ADDRESS, bootloader_macaddr,
                          sizeof(bootloader_macaddr), &actual_len);
  if (status != ZX_OK) {
    return status;
  } else if (actual_len < MAC_ADDR_LEN) {
    return ZX_ERR_INTERNAL;
  }
  memcpy(macaddr, bootloader_macaddr, MAC_ADDR_LEN);
  zxlogf(INFO, "bcm-hci: got bootloader mac address %02x:%02x:%02x:%02x:%02x:%02x\n", macaddr[0],
         macaddr[1], macaddr[2], macaddr[3], macaddr[4], macaddr[5]);

  return ZX_OK;
}

static int bcm_hci_start_thread(void* arg) {
  bcm_hci_t* hci = arg;
  zx_handle_t fw_vmo;

  zx_handle_t theirs;
  zx_status_t status = zx_channel_create(0, &hci->command_channel, &theirs);
  if (status != ZX_OK) {
    goto fail;
  }
  status = bt_hci_open_command_channel(&hci->hci, theirs);
  if (status != ZX_OK) {
    goto fail;
  }

  // Send Reset command
  status = bcm_hci_send_command(hci, &RESET_CMD, sizeof(RESET_CMD), NULL, 0);
  if (status != ZX_OK) {
    goto fail;
  }

  if (hci->is_uart) {
    // switch baud rate to TARGET_BAUD_RATE
    status = bcm_hci_set_baud_rate(hci, TARGET_BAUD_RATE);
    if (status != ZX_OK) {
      goto fail;
    }
  }

  size_t fw_size;
  status = load_firmware(hci->zxdev, FIRMWARE_PATH, &fw_vmo, &fw_size);
  if (status == ZX_OK) {
    status = bcm_hci_send_command(hci, &START_FIRMWARE_DOWNLOAD_CMD,
                                  sizeof(START_FIRMWARE_DOWNLOAD_CMD), NULL, 0);
    if (status != ZX_OK) {
      goto fail;
    }

    // give time for placing firmware in download mode
    zx_nanosleep(zx_deadline_after(FIRMWARE_DOWNLOAD_DELAY));

    zx_off_t offset = 0;
    while (offset < fw_size) {
      uint8_t buffer[255 + 3];

      size_t remaining = fw_size - offset;
      size_t read_amount = (remaining > sizeof(buffer) ? sizeof(buffer) : remaining);

      if (read_amount < 3) {
        zxlogf(ERROR, "short HCI command in firmware download\n");
        status = ZX_ERR_INTERNAL;
        goto vmo_close_fail;
      }

      status = zx_vmo_read(fw_vmo, buffer, offset, read_amount);
      if (status != ZX_OK) {
        goto vmo_close_fail;
      }

      hci_command_header_t* header = (hci_command_header_t*)buffer;
      size_t length = header->parameter_total_size + sizeof(*header);
      if (read_amount < length) {
        zxlogf(ERROR, "short HCI command in firmware download\n");
        status = ZX_ERR_INTERNAL;
        goto vmo_close_fail;
      }
      status = bcm_hci_send_command(hci, header, length, NULL, 0);
      if (status != ZX_OK) {
        zxlogf(ERROR, "bcm_hci_send_command failed in firmware download: %s\n",
               zx_status_get_string(status));
        goto vmo_close_fail;
      }
      offset += length;
    }

    zx_handle_close(fw_vmo);

    if (hci->is_uart) {
      // firmware switched us back to 115200. switch back to TARGET_BAUD_RATE
      status = serial_config(&hci->serial, 115200, SERIAL_SET_BAUD_RATE_ONLY);
      if (status != ZX_OK) {
        goto fail;
      }

      // switch baud rate to TARGET_BAUD_RATE after DELAY
      zx_nanosleep(zx_deadline_after(BAUD_RATE_SWITCH_DELAY));
      status = bcm_hci_set_baud_rate(hci, TARGET_BAUD_RATE);
      if (status != ZX_OK) {
        goto fail;
      }
    }
    zxlogf(INFO, "bcm-hci: firmware loaded\n");
  } else {
    zxlogf(ERROR, "bcm-hci: could not load firmware file: %s\n", zx_status_get_string(status));
    goto fail;
  }

  // set BDADDR to value in bootloader
  uint8_t macaddr[MAC_ADDR_LEN];
  status = bcm_get_bdaddr_from_bootloader(hci, macaddr);
  if (status == ZX_OK) {
    // send Set BDADDR command
    status = bcm_hci_set_bdaddr(hci, macaddr);
    if (status != ZX_OK) {
      goto fail;
    }
  } else {
    // log error and fallback mac address
    hci_read_bdaddr_command_complete_t event;
    memset(&event, 0, sizeof(event));
    char fallback_addr[18] = "<unknown>";
    zx_status_t read_cmd_status = bcm_hci_send_command(
        hci, &READ_BDADDR_CMD, sizeof(READ_BDADDR_CMD), (void*)(&event), sizeof(event));
    if (read_cmd_status == ZX_OK) {
      // HCI returns data as little endian. Swap bytes
      snprintf(fallback_addr, 18, "%02x:%02x:%02x:%02x:%02x:%02x", event.bdaddr[5], event.bdaddr[4],
               event.bdaddr[3], event.bdaddr[2], event.bdaddr[1], event.bdaddr[0]);
    }
    zxlogf(ERROR,
           "bcm-hci: error getting mac address from bootloader: %s. "
           "Fallback address: %s.\n",
           zx_status_get_string(status), fallback_addr);
  }

  // We're done with the command channel. Close it so that it can be opened by
  // the host stack after the device becomes visible.
  zx_handle_close(hci->command_channel);
  hci->command_channel = ZX_HANDLE_INVALID;

  device_make_visible(hci->zxdev);
  return 0;

vmo_close_fail:
  zx_handle_close(fw_vmo);
fail:
  zxlogf(ERROR, "bcm_hci_start_thread: device initialization failed: %s\n",
         zx_status_get_string(status));

  device_remove(hci->zxdev);
  return -1;
}

static zx_status_t bcm_hci_bind(void* ctx, zx_device_t* device) {
  bcm_hci_t* hci = calloc(1, sizeof(bcm_hci_t));
  if (!hci) {
    return ZX_ERR_NO_MEMORY;
  }

  zx_status_t status = device_get_protocol(device, ZX_PROTOCOL_BT_HCI, &hci->hci);
  if (status != ZX_OK) {
    zxlogf(ERROR, "bcm_hci_bind: get protocol ZX_PROTOCOL_BT_HCI failed\n");
    return status;
  }
  status = device_get_protocol(device, ZX_PROTOCOL_SERIAL, &hci->serial);
  if (status == ZX_OK) {
    hci->is_uart = true;
  }

  device_add_args_t args = {
      .version = DEVICE_ADD_ARGS_VERSION,
      .name = "bcm-hci",
      .ctx = hci,
      .ops = &bcm_hci_device_proto,
      .proto_id = ZX_PROTOCOL_BT_HCI,
      .flags = DEVICE_ADD_INVISIBLE,
  };

  hci->transport_dev = device;

  status = device_add(device, &args, &hci->zxdev);
  if (status != ZX_OK) {
    bcm_hci_release(hci);
    return status;
  }

  // create thread to continue initialization
  thrd_t t;
  int thrd_rc = thrd_create_with_name(&t, bcm_hci_start_thread, hci, "bcm_hci_start_thread");
  if (thrd_rc != thrd_success) {
    device_remove(hci->zxdev);
    bcm_hci_release(hci);
    return thrd_status_to_zx_status(thrd_rc);
  }

  return ZX_OK;
}

static zx_driver_ops_t bcm_hci_driver_ops = {
    .version = DRIVER_OPS_VERSION,
    .bind = bcm_hci_bind,
};

// clang-format off
ZIRCON_DRIVER_BEGIN(bcm_hci, bcm_hci_driver_ops, "zircon", "0.1", 2)
    BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_BT_TRANSPORT),
    BI_MATCH_IF(EQ, BIND_SERIAL_VID, PDEV_VID_BROADCOM),
ZIRCON_DRIVER_END(bcm_hci)
